﻿namespace Cofoundry.Domain;

/// <summary>
/// A small page object focused on providing routing data. A PageRoute returned from one of the
/// standard Cofoundry queries is typically stored in a cache to make sure access is fast.
/// </summary>
/// <inheritdoc/>
public class PageRoute : IPageRoute, IPublishableEntity
{
    /// <summary>
    /// Database identifier for the page.
    /// </summary>
    public int PageId { get; set; }

    /// <summary>
    /// Optional locale of the page.
    /// </summary>
    public ActiveLocale? Locale { get; set; }

    /// <summary>
    /// The directory this page is in.
    /// </summary>
    public PageDirectoryRoute PageDirectory { get; set; } = PageDirectoryRoute.Uninitialized;

    /// <summary>
    /// The path of the page within the directory. This will be
    /// unique within the directory the page is parented to.
    /// </summary>
    public string UrlPath { get; set; } = string.Empty;

    /// <summary>
    /// The full path of the page including directories and the locale. This 
    /// includes the leading slash, but excludes a trailing slash 
    /// e.g. "/my-directory/my-page".
    /// </summary>
    public string FullUrlPath { get; set; } = string.Empty;

    /// <summary>
    /// The title of the page for the currently published version, falling
    /// back to the draft version is there is no published version.
    /// </summary>
    public string Title { get; set; } = string.Empty;

    /// <summary>
    /// Indicates if the page is marked as published or not, which allows the page
    /// to be shown on the live site if the <see cref="PublishDate"/> has passed.
    /// </summary>
    public PublishStatus PublishStatus { get; set; }

    /// <summary>
    /// The date and time that the page was first published. This date can be set to a future date 
    /// to indicate that page should not appear on the live site until this date has passed.
    /// </summary>
    public DateTime? PublishDate { get; set; }

    /// <summary>
    /// The date and time that the page was last published. This can be different to
    /// <see cref="PublishDate"/> which is generally the date the page was originally
    /// published, with this property relecting any subsequent updates. The <see cref="PublishDate"/> 
    /// can be set manually to a future date when publishing, however the change is also 
    /// reflected in <see cref="LastPublishDate"/> if it is scheduled ahead of the existing 
    /// <see cref="LastPublishDate"/>.
    /// </summary>
    public DateTime? LastPublishDate { get; set; }

    /// <summary>
    /// Indicates whether there is a draft version of this page available.
    /// </summary>
    public bool HasDraftVersion { get; set; }

    /// <summary>
    /// Indicates whether there is a published version of this page available.
    /// </summary>
    public bool HasPublishedVersion { get; set; }

    /// <summary>
    /// Routing information particular to specific versions.
    /// </summary>
    public IReadOnlyCollection<PageVersionRoute> Versions { get; set; } = Array.Empty<PageVersionRoute>();

    /// <summary>
    /// Most pages are generic pages but they could have some sort of
    /// special function e.g. NotFound, CustomEntityDetails.
    /// </summary>
    public PageType PageType { get; set; }

    /// <summary>
    /// Indicates whether the page should show in the autogenerated site map
    /// that gets presented to search engine robots. Based on the draft version
    /// if no published version exists, so be careful to check the page is 
    /// published before making use of this.
    /// </summary>
    public bool ShowInSiteMap { get; set; }

    /// <summary>
    /// If this is of <see cref="PageType.CustomEntityDetails"/>, this is used
    /// to look up the routing.
    /// </summary>
    public string? CustomEntityDefinitionCode { get; set; }

    /// <summary>
    /// Optional set of rules that can be used to restrict access to this page.
    /// These rules are for this page only, and does not include rules
    /// associated with any parent directories. To check he full directory tree 
    /// use the <see cref="ValidateAccess"/> method. If there are no rules associated 
    /// with the page then this will be <see langword="null"/>.
    /// </summary>
    public EntityAccessRuleSet? AccessRuleSet { get; set; }

    /// <summary>
    /// Determines if the page is within the specified directory path. Does
    /// not return <see langword="true"/> if it is in a subdirectory of the 
    /// specified directory path.
    /// </summary>
    public bool IsInDirectory(string directoryPath)
    {
        if (PageDirectory == null)
        {
            return false;
        }

        if (Locale == null)
        {
            return PageDirectory.MatchesPath(directoryPath);
        }

        return PageDirectory.MatchesPath(directoryPath, Locale.LocaleId);
    }

    /// <summary>
    /// Indicates if this is the default file in the directory
    /// </summary>
    public bool IsDirectoryDefaultPage(int? localeId = null)
    {
        return string.IsNullOrWhiteSpace(UrlPath);
    }

    /// <summary>
    /// Determines if the <paramref name="user"/> can access the page by checking
    /// the access rules for the page and any parent directories.
    /// </summary>
    /// <param name="user">The <see cref="IUserContext"/> to check against access rules.</param>
    /// <returns>
    /// <see langword="true"/> if the user is permitted access; <see langword="false"/> if access
    /// rule violations were found.
    /// </returns>
    public bool CanAccess(IUserContext user)
    {
        return ValidateAccess(user) == null;
    }

    /// <summary>
    /// Determines if the <paramref name="user"/> violates any access rules
    /// for this page or any parent directories. If the user cannot access the 
    /// page then the violated <see cref="EntityAccessRuleSet"/> is returned. The user may 
    /// violate several rules in the page and directory tree but only the most specific rule set is 
    /// returned, starting with the page and then working back up through the 
    /// directory tree. 
    /// </summary>
    /// <param name="user">The <see cref="IUserContext"/> to check against access rules.</param>
    /// <returns>
    /// If any rules are violated, then the most specific <see cref="EntityAccessRuleSet"/> is returned; 
    /// otherwise <see langword="null"/>.
    /// </returns>
    public EntityAccessRuleSet? ValidateAccess(IUserContext user)
    {
        ArgumentNullException.ThrowIfNull(user);
        EntityInvalidOperationException.ThrowIfNull(this, r => r.PageDirectory);

        if (AccessRuleSet != null && !AccessRuleSet.IsAuthorized(user))
        {
            return AccessRuleSet;
        }

        var directoryViolation = PageDirectory
            .AccessRuleSets
            .GetRuleViolations(user)
            .FirstOrDefault();

        return directoryViolation;
    }

    /// <summary>
    /// A placeholder value to use for not-nullable values that you
    /// know will be initialized in later code. This value should not
    /// be used in data post-initialization.
    /// </summary>
    public static readonly PageRoute Uninitialized = new()
    {
        PageId = int.MinValue
    };
}
